<template><div><h2 id="加密" tabindex="-1"><a class="header-anchor" href="#加密"><span>加密</span></a></h2>
<p>Yii2 的安全组件类 <code v-pre>\yii\base\Security</code>，提供了一系列方法来处理与安全性相关的常见任务，其中当然也包括加密相关的方法，如生成随机数据、加密和解密及确认数据完整性等。</p>
<h2 id="生成伪随机数据" tabindex="-1"><a class="header-anchor" href="#生成伪随机数据"><span>生成伪随机数据</span></a></h2>
<p>伪随机数据在很多情况下都很有用。例如，当通过电子邮件重置密码时，您需要生成一个令牌，将其保存到数据库中，并通过电子邮件发送给最终用户，这反过来又会允许他们证明该帐户的所有权。这个令牌是独一无二且难以猜测的，否则攻击者可能会预测令牌的值并重置用户的密码。</p>
<p>我们可以用 Yii 的安全组件来简单生成伪随机数据，即生成指定长度的随机字符串，该字符串匹配 <code v-pre>[A-Za-z0-9_-]+</code>，对 URL 编码是透明的。代码如下：</p>
<div class="language-php line-numbers-mode" data-highlighter="prismjs" data-ext="php" data-title="php"><pre v-pre class="language-php"><code><span class="line"><span class="token variable">$key</span> <span class="token operator">=</span> <span class="token class-name static-context">Yii</span><span class="token operator">::</span><span class="token variable">$app</span><span class="token operator">-></span><span class="token property">security</span><span class="token operator">-></span><span class="token function">generateRandomString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span>
<span class="line"></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><h2 id="生成随机字节" tabindex="-1"><a class="header-anchor" href="#生成随机字节"><span>生成随机字节</span></a></h2>
<p>Yii 提供了 <code v-pre>generateRandomKey</code> 方法来生成随机字节，和 <code v-pre>generateRandomString</code> 方法类似，生成一个随机的串，参数为长度，默认为32位，区别在于它生成的可能不是 ASCII，代码如下：</p>
<div class="language-php line-numbers-mode" data-highlighter="prismjs" data-ext="php" data-title="php"><pre v-pre class="language-php"><code><span class="line"><span class="token variable">$inputKey</span> <span class="token operator">=</span> <span class="token class-name static-context">Yii</span><span class="token operator">::</span><span class="token variable">$app</span><span class="token operator">-></span><span class="token property">security</span><span class="token operator">-></span><span class="token function">generateRandomKey</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span>
<span class="line"></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><h2 id="密钥推导" tabindex="-1"><a class="header-anchor" href="#密钥推导"><span>密钥推导</span></a></h2>
<h3 id="hkdf-算法" tabindex="-1"><a class="header-anchor" href="#hkdf-算法"><span>HKDF 算法</span></a></h3>
<p>使用标准 HKDF 算法从给定的输入键导出一个键。代码如下：</p>
<div class="language-php line-numbers-mode" data-highlighter="prismjs" data-ext="php" data-title="php"><pre v-pre class="language-php"><code><span class="line"><span class="token variable">$key</span> <span class="token operator">=</span> <span class="token class-name static-context">Yii</span><span class="token operator">::</span><span class="token variable">$app</span><span class="token operator">-></span><span class="token property">security</span><span class="token operator">-></span><span class="token function">hkdf</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'sha256'</span><span class="token punctuation">,</span> <span class="token variable">$inputKey</span><span class="token punctuation">,</span> <span class="token variable">$salt</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span>
<span class="line"></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><h3 id="hkdf2-算法" tabindex="-1"><a class="header-anchor" href="#hkdf2-算法"><span>HKDF2 算法</span></a></h3>
<p>使用标准的 PBKDF2 算法从给定的密码导出一个密钥。代码如下：</p>
<div class="language-php line-numbers-mode" data-highlighter="prismjs" data-ext="php" data-title="php"><pre v-pre class="language-php"><code><span class="line"><span class="token variable">$key</span> <span class="token operator">=</span> <span class="token class-name static-context">Yii</span><span class="token operator">::</span><span class="token variable">$app</span><span class="token operator">-></span><span class="token property">security</span><span class="token operator">-></span><span class="token function">hkdf</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'sha256'</span><span class="token punctuation">,</span> <span class="token variable">$password</span><span class="token punctuation">,</span> <span class="token variable">$salt</span><span class="token punctuation">,</span> <span class="token number">100000</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span>
<span class="line"></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><h2 id="加密和解密" tabindex="-1"><a class="header-anchor" href="#加密和解密"><span>加密和解密</span></a></h2>
<p>Yii 提供了两种 <strong>加密/解密</strong> 数据的方式。数据通过加密功能传递，以便只有拥有密钥的人才能解密。例如，我们需要在数据库中存储一些信息，但我们需要确保只有拥有密钥的用户才能查看它（即使应用程序数据库已被泄露）。</p>
<h3 id="密码方式" tabindex="-1"><a class="header-anchor" href="#密码方式"><span>密码方式</span></a></h3>
<p>我们可以使用 <code v-pre>\yii\base\Security::encryptByPassword()</code> 方法，使用密码方式进行数据加密，该方法对一个密码，使用了 PBKDF2 和一个随机 salt 来获取用于加密和身份验证的键，该随机 salt 故意放慢速度以防止字典攻击。键的派生时间由 <code v-pre>\yii\base\Security::$derivationIterations</code> 属性决定，应该将其设置得尽可能高。具体代码如下：</p>
<div class="language-php line-numbers-mode" data-highlighter="prismjs" data-ext="php" data-title="php"><pre v-pre class="language-php"><code><span class="line"><span class="token comment">// $data 和 $secretKey 从表单中获得</span></span>
<span class="line"><span class="token variable">$encryptedData</span> <span class="token operator">=</span> <span class="token class-name static-context">Yii</span><span class="token operator">::</span><span class="token variable">$app</span><span class="token operator">-></span><span class="token property">security</span><span class="token operator">-></span><span class="token function">encryptByPassword</span><span class="token punctuation">(</span><span class="token variable">$data</span><span class="token punctuation">,</span> <span class="token variable">$secretKey</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span>
<span class="line"><span class="token comment">// 将 $encryptedData 存储到数据库</span></span>
<span class="line"></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><blockquote>
<p>注意: 尽可能避免使用密码加密。没有任何东西可以保护低质量或被破解的密码。</p>
</blockquote>
<p>对应的，我们可以使用 <code v-pre>\yii\base\Security:: decryptByPassword()</code> 方法，来通过密码方式解密，即随后当用户想要读取数据时，代码如下：</p>
<div class="language-php line-numbers-mode" data-highlighter="prismjs" data-ext="php" data-title="php"><pre v-pre class="language-php"><code><span class="line"><span class="token comment">// $secretKey 从用户输入获得，$encryptedData 来自数据库</span></span>
<span class="line"><span class="token variable">$data</span> <span class="token operator">=</span> <span class="token class-name static-context">Yii</span><span class="token operator">::</span><span class="token variable">$app</span><span class="token operator">-></span><span class="token property">security</span><span class="token operator">-></span><span class="token function">decryptByPassword</span><span class="token punctuation">(</span><span class="token variable">$encryptedData</span><span class="token punctuation">,</span> <span class="token variable">$secretKey</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span>
<span class="line"></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div></div></div><blockquote>
<p>注意: 通过上面得到的编码后的数据不是 ASCII，可以通过 base64_encode() 和 base64_decode() 在外层包装下。</p>
</blockquote>
<h3 id="密钥方式" tabindex="-1"><a class="header-anchor" href="#密钥方式"><span>密钥方式</span></a></h3>
<p>我们可以使用 <code v-pre>\yii\base\Security::encryptByKey()</code> 方法，使用密钥方式进行数据加密，该方法对一个密钥，使用了 HKDF 和 一个随机 salt 来获取用于加密和身份验证的键，相对于<code v-pre>encryptByPassword()</code> 来说非常快。密钥必须是适当随机的，可以使用 <code v-pre>generateRandomKey()</code> 来生成。代码如下：</p>
<div class="language-php line-numbers-mode" data-highlighter="prismjs" data-ext="php" data-title="php"><pre v-pre class="language-php"><code><span class="line"><span class="token comment">// 生成密钥</span></span>
<span class="line"><span class="token variable">$inputKey</span> <span class="token operator">=</span> <span class="token class-name static-context">Yii</span><span class="token operator">::</span><span class="token variable">$app</span><span class="token operator">-></span><span class="token property">security</span><span class="token operator">-></span><span class="token function">generateRandomKey</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span>
<span class="line"><span class="token comment">// 密钥加密</span></span>
<span class="line"><span class="token variable">$encryptedData</span> <span class="token operator">=</span> <span class="token class-name static-context">Yii</span><span class="token operator">::</span><span class="token variable">$app</span><span class="token operator">-></span><span class="token property">security</span><span class="token operator">-></span><span class="token function">encryptByKey</span><span class="token punctuation">(</span><span class="token variable">$data</span><span class="token punctuation">,</span> <span class="token variable">$inputKey</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span>
<span class="line"></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div><div class="line-number"></div></div></div><p>对应的，我们也可以通过 <code v-pre>\yii\base\Security::decryptByKey()</code> 使用密钥来解密，代码如下：</p>
<div class="language-php line-numbers-mode" data-highlighter="prismjs" data-ext="php" data-title="php"><pre v-pre class="language-php"><code><span class="line"><span class="token comment">//解密</span></span>
<span class="line"><span class="token variable">$data</span> <span class="token operator">=</span> <span class="token class-name static-context">Yii</span><span class="token operator">::</span><span class="token variable">$app</span><span class="token operator">-></span><span class="token property">security</span><span class="token operator">-></span><span class="token function">decryptByKey</span><span class="token punctuation">(</span><span class="token variable">$encryptedData</span><span class="token punctuation">,</span> <span class="token variable">$inputKey</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span>
<span class="line"></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div></div></div><blockquote>
<p>注意: 该加密解密方法，存在着第三个参数，比如我们可以传用户的 ID 等，这样此信息将和 $inputKey 一起作为加密解密的钥匙。</p>
</blockquote>
<p>以上两个加密的数据包含一个密钥消息验证码（MAC），因此不需要对输入或输出数据进行哈希处理。</p>
<h2 id="确认数据完整性" tabindex="-1"><a class="header-anchor" href="#确认数据完整性"><span>确认数据完整性</span></a></h2>
<p>在某些情况下，您需要验证您的数据未被第三方篡改，甚至以某种方式损坏。Yii 提供了一种简单的方法来确认数据完整性的。</p>
<p>用密钥和数据生成的哈希前缀数据，以便在数据被篡改后能够被检测到，代码如下：</p>
<div class="language-php line-numbers-mode" data-highlighter="prismjs" data-ext="php" data-title="php"><pre v-pre class="language-php"><code><span class="line"><span class="token comment">// $secretKey 是我们的应用程序或用户密钥，$genuineData 是从可靠来源获得的</span></span>
<span class="line"><span class="token variable">$data</span> <span class="token operator">=</span> <span class="token class-name static-context">Yii</span><span class="token operator">::</span><span class="token variable">$app</span><span class="token operator">-></span><span class="token property">security</span><span class="token operator">-></span><span class="token function">hashData</span><span class="token punctuation">(</span><span class="token variable">$genuineData</span><span class="token punctuation">,</span> <span class="token variable">$secretKey</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span>
<span class="line"></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div></div></div><blockquote>
<p>注意：<code v-pre>hashData()</code> 的第三个参数代表生成的哈希值是否为原始二进制格式，如果为 <code v-pre>false</code>，则会生成小写十六进制数字。</p>
</blockquote>
<p>检查数据完整性是否受到损害，代码如下：</p>
<div class="language-php line-numbers-mode" data-highlighter="prismjs" data-ext="php" data-title="php"><pre v-pre class="language-php"><code><span class="line"><span class="token comment">// $secretKey 我们的应用程序或用户密钥，$data 从不可靠的来源获得</span></span>
<span class="line"><span class="token variable">$data</span> <span class="token operator">=</span> <span class="token class-name static-context">Yii</span><span class="token operator">::</span><span class="token variable">$app</span><span class="token operator">-></span><span class="token function">getSecurity</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">validateData</span><span class="token punctuation">(</span><span class="token variable">$data</span><span class="token punctuation">,</span> <span class="token variable">$secretKey</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span>
<span class="line"></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div><div class="line-number"></div></div></div><blockquote>
<p>注意：<code v-pre>validateData()</code> 函数的第三个参数应该与使用 <code v-pre>hashData()</code> 生成数据时的值相同，它指示数据中的 hash 值是否是二进制格式，如果为 <code v-pre>false</code>，则表示 hash 值仅由小写十六进制数字组成，将生成十六进制数字。</p>
</blockquote>
<blockquote>
<p>注意：当这些方法执行任务时，不需要对 <code v-pre>encryptByKey()</code> 或 <code v-pre>encryptByPassword()</code> 的输入或输出进行哈希处理。</p>
</blockquote>
<h2 id="时序攻击防护" tabindex="-1"><a class="header-anchor" href="#时序攻击防护"><span>时序攻击防护</span></a></h2>
<p>在密码学中，时序攻击是一种侧信道攻击，攻击者试图通过分析加密算法的时间执行来推导出密码。Yii 提供了一个简单的防时序攻击的方法进行字符串比较，代码如下：</p>
<div class="language-php line-numbers-mode" data-highlighter="prismjs" data-ext="php" data-title="php"><pre v-pre class="language-php"><code><span class="line"><span class="token class-name static-context">Yii</span><span class="token operator">::</span><span class="token variable">$app</span><span class="token operator">-></span><span class="token property">security</span><span class="token operator">-></span><span class="token function">compareString</span><span class="token punctuation">(</span><span class="token variable">$expected</span><span class="token punctuation">,</span> <span class="token variable">$actual</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span>
<span class="line"></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><h2 id="breach-攻击防护" tabindex="-1"><a class="header-anchor" href="#breach-攻击防护"><span>BREACH 攻击防护</span></a></h2>
<p>BREACH（Browser Reconnaissance and Exfiltration via Adaptive Compression of Hypertext，中文为：通过自适应超文本压缩的浏览器侦察和外泄）攻击，是一个当使用 HTTP 压缩时，针对 HTTPS 的安全漏洞。</p>
<p>Yii 从 2.0.12 开始，提供了一组防护 BREACH 攻击的方法，将随机掩码应用于 token，并将用于结果的掩码前置，使字符串始终惟一。通过在每个请求上随机输出 token 来减少 BREACH 攻击。</p>
<p>给 token 增加掩码，以使其不可压缩，代码如下：</p>
<div class="language-php line-numbers-mode" data-highlighter="prismjs" data-ext="php" data-title="php"><pre v-pre class="language-php"><code><span class="line"><span class="token variable">$maskedToken</span> <span class="token operator">=</span> <span class="token class-name static-context">Yii</span><span class="token operator">::</span><span class="token variable">$app</span><span class="token operator">-></span><span class="token property">security</span><span class="token operator">-></span><span class="token function">maskToken</span><span class="token punctuation">(</span><span class="token variable">$token</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span>
<span class="line"></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><p>给之前的 token 解除掩码，代码如下：</p>
<div class="language-php line-numbers-mode" data-highlighter="prismjs" data-ext="php" data-title="php"><pre v-pre class="language-php"><code><span class="line"><span class="token variable">$token</span> <span class="token operator">=</span> <span class="token class-name static-context">Yii</span><span class="token operator">::</span><span class="token variable">$app</span><span class="token operator">-></span><span class="token property">security</span><span class="token operator">-></span><span class="token function">unmaskToken</span><span class="token punctuation">(</span><span class="token variable">$maskedToken</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span>
<span class="line"></span></code></pre>
<div class="line-numbers" aria-hidden="true" style="counter-reset:line-number 0"><div class="line-number"></div></div></div><blockquote>
<p>注意：在 Yii2 的 csrf 功能上就使用了 maskToken。</p>
</blockquote>
<blockquote>
<p>💖喜欢本文档的，欢迎点赞、收藏、留言或转发，谢谢支持！<br>
作者邮箱：zhuzixian520@126.com，github地址：<a href="https://github.com/zhuzixian520" target="_blank" rel="noopener noreferrer">github.com/zhuzixian520</a></p>
</blockquote>
</div></template>


